package cz.muni.fi.pb138.accountingRecords;



import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.exist.xmldb.EXistResource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;
/**
 * Implementation of Payment manager, provides operationes over payments,
 * uses eXist database at port 8899
 * 
 * @author Jaroslav Klech
 */
public class PaymentManagerImpl implements PaymentManager{
    private static String URI = "xmldb:exist://localhost:8899/exist/xmlrpc";
    private static final String driver = "org.exist.xmldb.DatabaseImpl";
    private String collectionName;
    private String user;
    private String password;
    Database database;
    private static final Logger logger = Logger.getLogger(PaymentManagerImpl.class.getName());
    
    
    /**
     * Constructor for Payment manager implementation
     * 
     * @param collection payments Colection in eXist database
     * @param user user name for eXist database
     * @param password password for eXist database
     */
    public PaymentManagerImpl(String collection, String user, String password){
        this.collectionName = collection;
        this.user = user;
        this.password = password;
        
        try{
            Class cl = Class.forName(driver);

            database = (Database) cl.newInstance();
            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);
        }catch(Exception ex){
            logger.log(Level.SEVERE, "Loading database failed", ex);
            throw new ServiceFailureException("Loading database failed: ",ex);
        }
    }
    
    @Override
    public void addPayment(Payment payment) {
        if(payment == null){
            throw new IllegalArgumentException("Payment is null");
        }
        if(payment.getId() != null){
            throw new IllegalArgumentException("Payment is already in database");
        }
        validatePayment(payment);
        
        Collection col;
        col = this.getCollection();
        if (col == null) {
            logger.log(Level.SEVERE, "XMLDatabase error: addPayment, getting collection failed ");
            throw new ServiceFailureException("XMLDatabase error: getting collection failed");
        }
        
        String id = null;
        try{
            id = col.createId();
        }catch(XMLDBException ex){
            logger.log(Level.SEVERE, "XMLDatabase error: addPAyment, creating ID failed ", ex);
            throw new ServiceFailureException("Can't create ID", ex);
        }
        //transfer payment object to document
        Document doc = transferToDocument(payment);
        XMLValidator validator = new XMLValidator("payment.xsd");
        validator.checkXML(doc);

        XMLResource res = null;
        try{
            res = (XMLResource) col.createResource(id, "XMLResource");
            res.setContentAsDOM(doc);
            col.storeResource(res);
        }catch(Exception ex){
            logger.log(Level.SEVERE, "XMLDatabase error: addPAyment, error saving payment to DTB ", ex);
            throw new ServiceFailureException("Error while saving payment DOM to DTB",ex);
        }finally{
            cleanUpResources(res,col);
        }
        //uspesne ulozene pridam id
        payment.setId(id);
        
    }    
    



    @Override
    public Payment getPayment(String id) {
        if(id == null ||id.isEmpty()){
            throw new IllegalArgumentException("Payment id is null");
        }
        
        Collection col;
        col = this.getCollection();
        if (col == null) {
            logger.log(Level.SEVERE, "XMLDatabase error: getPayment, getting collection has failed ");
            throw new ServiceFailureException("XMLDatabase error: getting collection failed");
        }
        Payment payment = null;
        XMLResource res = null;
        try{
            res = (XMLResource)col.getResource(id);
            if(res == null){
                return null;
            }
            
            Document doc = (Document) res.getContentAsDOM();
            if (doc == null) {
                logger.log(Level.SEVERE, "XMLDatabase error: getPayment, getting document has failed ");
                throw new ServiceFailureException("Dom Node is null !");
            }
            XMLValidator validator = new XMLValidator("payment.xsd");
            validator.checkXML(doc); 
            
            payment = transferFromDocument(doc,res,id);
            
        }catch(XMLDBException ex){
            logger.log(Level.SEVERE, "XMLDatabase error: getPayment, Error while selecting payment from DB ", ex);
            throw new ServiceFailureException("Error while selecting payment from DB",ex);
        }finally{
            cleanUpResources(res,col);
        }
        
        return payment;
    }

    @Override
    public void removePayment(String id) {
        if(id == null || id.isEmpty()){
            throw new IllegalArgumentException("Remove payment: Id is null");
        }
        
        Collection col;
        col = this.getCollection();
        if (col == null) {
            logger.log(Level.SEVERE, "XMLDatabase error: removePayment, getting collection has failed ");
            throw new ServiceFailureException("XMLDatabase error: getting collection failed");
        }
        
        Resource res = null;
        try{
            //find payment
            res = col.getResource(id);
            if(res == null){
                logger.log(Level.SEVERE, "XMLDatabase error: removePayment, getting resource has failed id: {0}", id);
                throw new ServiceFailureException("Remove payment: payment with id: " +id+" doesn't exist");
            }
            //delete payment
            col.removeResource(res);
        }catch(XMLDBException ex){
            logger.log(Level.SEVERE, "XMLDatabase error: removePayment, removing payment has failed ",ex);
            throw new ServiceFailureException("Error while removing payment from DB",ex);
        }finally{
            cleanUpResources((XMLResource)res,col);
        }
        
    }

    @Override
    public void updatePayment(Payment payment) {
        if(payment == null){
            throw new IllegalArgumentException("Update payment: payment is null");
        }
        if(payment.getId() == null || payment.getId().isEmpty()){
            throw new IllegalArgumentException("Update payment: payment's id is empty");
        }
        validatePayment(payment);
        
        Collection col = this.getCollection();
        if (col == null) {
            logger.log(Level.SEVERE, "XMLDatabase error: updatePayment, getting collection has failed ");
            throw new ServiceFailureException("XMLDatabase error: getting collection failed");
        } 
        XMLResource res = null;
        try{
            res = (XMLResource)col.getResource(payment.getId());
            if(res == null){
                logger.log(Level.SEVERE, "XMLDatabase error: updatePayment, getting resource has failed id: {0}", payment);
                throw new ServiceFailureException("Update payment: payment doesn't exist");
            }
            Document doc = transferToDocument(payment);
            
            XMLValidator validator = new XMLValidator("payment.xsd");
            validator.checkXML(doc); 
            
            res.setContentAsDOM(doc);
            col.storeResource(res);
        }catch(XMLDBException ex){
            logger.log(Level.SEVERE, "XMLDatabase error: updatePayment, updating payment has failed: "+payment,ex);
            throw new ServiceFailureException("Error while updating payment in DB",ex);
        }finally{
            cleanUpResources(res,col);
        }
    }

    @Override
    public List<Payment> getPaymentsForPeriod(Date from, Date to) {
        if(from == null){
            throw new IllegalArgumentException("Get Payments For Period: From is null");
        }
        if(to == null){
            throw new IllegalArgumentException("Get Payments For Period: To is null");
        }
        
        if(from.after(to)){
            Date temp = from;
            from = to;
            to = temp;
        }
        
        Collection col = this.getCollection();
        if (col == null) {
            logger.log(Level.SEVERE, "XMLDatabase error: getPaymentsForPeriod, getting collection has failed ");
            throw new ServiceFailureException("XMLDatabase error: getting collection failed");
        } 
        
        String[] paymentsIds;
        try{
            paymentsIds = col.listResources();
        }catch(XMLDBException ex){
            logger.log(Level.SEVERE, "XMLDatabase error: getPaymentsForPeriod, getting payment's ids has failed ",ex);
            throw new ServiceFailureException("Get Payments For Period: retrieving payments failed");
        }
        List<Payment> list = new ArrayList<Payment>();
        XMLResource res = null;
        try{
            XMLValidator validator = new XMLValidator("payment.xsd");
            for(String id:paymentsIds){
                res = (XMLResource)col.getResource(id);
                if(res == null){
                    logger.log(Level.SEVERE, "XMLDatabase error: getPaymentsForPeriod, getting resource has failed id");
                    throw new ServiceFailureException("Get Payments For Period: inconsistent database");
                }
                Document doc = (Document)res.getContentAsDOM();
                if(doc == null){
                    logger.log(Level.SEVERE, "XMLDatabase error: getPaymentsForPeriod, getting document has failed ");
                    throw new ServiceFailureException("Get Payments For Period: inconsistent database");
                }
                
                validator.checkXML(doc); 
                
                Payment payment = transferFromDocument(doc,res,id);
                Date date = payment.getTransferDate();
                if((from.before(date) || from.equals(date)) && (to.after(date) || to.equals(date))){
                    list.add(payment);
                }
            }
        }catch(Exception ex){
            logger.log(Level.SEVERE, "XMLDatabase error: getPaymentsForPeriod, getting payments has failed ",ex);
            throw new ServiceFailureException("Error while getting payments from DB",ex);
        }finally{
            cleanUpResources(res,col);
        }
        
        return list;
        

    }

    
    @Override
    public BigDecimal paymentSum(Date from, Date to){
        List<Payment> list = getPaymentsForPeriod(from,to);
        BigDecimal sum = new BigDecimal(0);
        for(Payment payment: list){
            sum = sum.add(payment.getAmount());
        }
        
        return sum;
    }
    
    //validates payments attributes
    private void validatePayment(Payment payment){
        if(payment.getBankAcc() == null || !payment.getBankAcc().matches("[1-9][0-9]{8}/[0-9]{4}")){
            throw new IllegalArgumentException("Receiver bank account format is wrong");
        }
        if(payment.getDestinationAcc() == null || !payment.getDestinationAcc().matches("[1-9][0-9]{8}/[0-9]{4}")){
            throw new IllegalArgumentException("Sender bank account format is wrong");
        }
        if(payment.getAmount() == null || payment.getAmount().compareTo(new BigDecimal(0)) <= 0){
            throw new IllegalArgumentException("Amount is lower than 0");
        }
        if(payment.getVariableSymbol()!=null && (payment.getVariableSymbol() < 0 || payment.getVariableSymbol() > 999999999)){
            throw new IllegalArgumentException("Incorect variable symbol");
        }
        if(payment.getTransferDate() == null){
             throw new IllegalArgumentException("Transfer date is missing");
        }
    }

    // transfers payment object to Document
    private Document transferToDocument(Payment payment){
        Document doc;

        try{
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            //root element
            doc = docBuilder.newDocument();
            doc.setXmlVersion("1.0");
            Element elmPayment = doc.createElement("payment");
            doc.appendChild(elmPayment);
            //sender acc
            Element elmSenderBankAccount = doc.createElement("sender_bank_account");
            elmSenderBankAccount.setTextContent(payment.getBankAcc());
            elmPayment.appendChild(elmSenderBankAccount);
            //receiver acc
            Element elmRecieverBankAccount = doc.createElement("reciever_bank_account");
            elmRecieverBankAccount.setTextContent(payment.getDestinationAcc());
            elmPayment.appendChild(elmRecieverBankAccount);
            //amount
            Element elmAmount = doc.createElement("amount");
            elmAmount.setTextContent(payment.getAmount().toString());
            elmPayment.appendChild(elmAmount);
            //variable_symbol
            if(payment.getVariableSymbol ()!= null){
                Element elmVariableSymbol = doc.createElement("variable_symbol");
                elmVariableSymbol.setTextContent(payment.getVariableSymbol().toString());
                elmPayment.appendChild(elmVariableSymbol);
            }
            //transfer date
            Element elmTransferDate = doc.createElement("transfer_date");
            SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ss");
                
            elmTransferDate.setTextContent(format.format(payment.getTransferDate()));
            elmPayment.appendChild(elmTransferDate);
            if(payment.getNote() != null && !payment.getNote().isEmpty()){
                Element elmNote = doc.createElement("note");
                elmNote.setTextContent(payment.getNote());
                elmPayment.appendChild(elmNote);
            }
            
            
            
        }catch(ParserConfigurationException ex){
            logger.log(Level.SEVERE, "Parse error: transferToDocument, transfering payment to document has failed ",ex);
            throw new ServiceFailureException("Error while creating payment DOM",ex);
        }
        return doc;
        
    }
    
    //transfer from document to Payment object
    private Payment transferFromDocument(Document doc,XMLResource res,String id){
            Payment payment = new Payment();
            payment.setId(id);
            
            //SenderBankAccount
            try{
                NodeList nodeListSender = doc.getElementsByTagName("sender_bank_account");
                if(nodeListSender.getLength() > 1){
                    throw new ServiceFailureException("More than one sender_bank_account element in payment: " + res.getContent());
                }
                Element elemSenderBankAccount = (Element)nodeListSender.item(0);
                payment.setBankAcc(elemSenderBankAccount.getTextContent());
                //ReceiverBankAccount
                NodeList nodeListReceiver = doc.getElementsByTagName("reciever_bank_account");
                if(nodeListReceiver.getLength() > 1){
                    throw new ServiceFailureException("More than one receiver_bank_account element in payment: " + res.getContent());
                }
                Element elemReceiverBankAccount = (Element)nodeListReceiver.item(0);            
                payment.setDestinationAcc(elemReceiverBankAccount.getTextContent());
                //amount
                NodeList nodeListAmount = doc.getElementsByTagName("amount");
                if(nodeListAmount.getLength() > 1){
                    throw new ServiceFailureException("More than one amount element in payment: " + res.getContent());
                }
                Element elemAmount = (Element)nodeListAmount.item(0);            
                payment.setAmount(new BigDecimal(elemAmount.getTextContent()));  
                //variable_symbol
                NodeList nodeListVariableSymbol = doc.getElementsByTagName("variable_symbol");
                if(nodeListVariableSymbol.getLength() > 1){
                    throw new ServiceFailureException("More than one variable_symbol element in payment: " + res.getContent());
                }
                if(nodeListVariableSymbol.getLength() == 0){
                    payment.setVariableSymbol(null);
                }
                else{
                    Element elemVariableSymbol = (Element)nodeListVariableSymbol.item(0);
                    payment.setVariableSymbol(Long.parseLong(elemVariableSymbol.getTextContent()));
                }
                //transfer_date
                NodeList nodeListTransferDate = doc.getElementsByTagName("transfer_date");
                if(nodeListTransferDate.getLength() > 1){
                    throw new ServiceFailureException("More than one transfer_date element in payment: " + res.getContent());
                }
                Element elemTransferDate = (Element)nodeListTransferDate.item(0);
                SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ss");
                payment.setTransferDate(format.parse(elemTransferDate.getTextContent()));
                //note
                NodeList nodeListNote = doc.getElementsByTagName("note");
                if(nodeListNote.getLength() > 1){
                    throw new ServiceFailureException("More than one note element in payment: " + res.getContent());
                }
                if(nodeListNote.getLength() == 1){
                    Element elemNote = (Element)nodeListNote.item(0);
                    payment.setNote(elemNote.getTextContent());
                }
            }catch(Exception ex){
                logger.log(Level.SEVERE, "Parse error: transferFromDocument, transfering payment from document has failed ",ex);
                throw new ServiceFailureException("Error transfering from document",ex);
            }
            return payment;
    }
        

    
    //get collection from DB
    private Collection getCollection() throws ServiceFailureException {
        Collection col = null;
        try {
            col = DatabaseManager.getCollection(URI + collectionName, user, password);
        } catch (XMLDBException ex) {
            logger.log(Level.SEVERE, "XMLDatabase error: getting collection failed.", ex);
            throw new ServiceFailureException("XMLDatabase error: getting collection failed :" + ex);
        }
        return col;
    }

        /*
     * frees resources and close connection to database
     */
    private void cleanUpResources(XMLResource res, Collection col) {
        if (res != null) {
            try {
                ((EXistResource) res).freeResources();
            } catch (Exception ex) {
                logger.log(Level.SEVERE, "Exception occured during freeing resources", ex);
            }
        }
        if (col != null) {
            try {
                col.close();
            } catch (Exception ex) {
                logger.log(Level.SEVERE, "Exception occured during closing connection", ex);
            }
        }
    }
}
